其他
从单体到微服务,腾讯文档微服务网关工程化的演进实践
👉导读
腾讯文档网关既承担着流量入口角色,又面临复杂的多适配逻辑,历经多次迭代后从单体演变为了微服务架构。本文是腾讯文档微服务网关工程化的演进实践总结,为你分享从 node Monorepo 微服务架构下使用 pnpm 与 Docker 构建的优化与思考。👉目录
1 现有问题2 工程化思考3 优化过程4 优化成果5 总结01
1.1 TL;DR
1.2 问题表现
1.3 问题根源
1.4 为什么锁不住?
@svr/proxy@0.0.123 /app
`-- @wgw/tools@0.0.123
+-- @opentelemetry/exporter-trace-otlp-grpc@0.27.0
| `-- @grpc/grpc-js@1.9.5
+-- @tencent/polaris@0.4.3
| `-- @grpc/grpc-js@1.9.5 deduped
+-- @tencent/tdocs-common-rainbow@0.0.4
| +-- @tencent/rainbow-node-admin-sdk@0.2.56
| | +-- @tencent/polaris@0.4.5
| | | `-- @grpc/grpc-js@1.9.5 deduped
| | `-- @tencent/trpc-rpc-client@0.5.39
| | `-- @tencent/trpc-rpc-naming@0.5.39
| | `-- @tencent/polaris@0.3.36
| | `-- @grpc/grpc-js@1.3.8
| `-- @tencent/rainbow-node-sdk@0.2.66
| `-- @tencent/polaris@0.5.2
| `-- @grpc/grpc-js@1.9.5 deduped
`-- @tencent/trpc-rpc-client@0.7.3
`-- @tencent/trpc-rpc-naming@0.7.3
`-- @tencent/polaris@0.5.2 // ^0.5.2
`-- @grpc/grpc-js@1.9.5 deduped
根目录的 @grpc/grpc-js 的依赖就是 1.9.5
1.5 为什么没有使用 lock 文件?
> tree -L 2 -I "node_modules"
.
├── lerna.json
├── package.json
├── packages -- 这里是四个微服务共用的包逻辑 @wgw/xxx
│ ├── deploy
│ ├── helpers
│ ├── middleware
│ ├── proto
│ ├── rpc
│ ├── tools
│ ├── types
│ └── utils
├── README.md
├── servers -- 四个微服务的服务入口 @svr/xxx
│ ├── cgi
│ ├── edit
│ ├── proxy
│ └── static
├── tsconfig.base.json
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
1.6 确定优化方向
Docker 镜像打包需要打包完整的 node_modules 且优化后项目启动入口文件不要有过多变化。 工程化工具需要解决间接依赖锁定版本问题,并能自定义依赖的具体版本。
02
2.1 高内聚低耦合
2.2 代码组织的极端
2.3 pnpm workspace
03
3.1 Docker 镜像构建
3.2 Docker COPY 与 node_modules
COPY . .
FROM base AS topdep
WORKDIR /
RUN mkdir node_modules packages
COPY node_modules/ node_modules
COPY packages/ packages
FROM topdep as svr
WORKDIR /usr/app
COPY ["pnpm-*.yaml", ".npmrc", "./"]
COPY "servers/${APP}/" .
3.3 Docker context
tar --exclude='node_modules' --exclude='.git' -cf - ../../ | docker build -f servers/proxy/Dockerfile.context - -t contextproxysvr
3.4 pnpm-context.mjs
// .... 这里只展示入口逻辑
async function main(cli) {
const projectPath = dirname(cli.dockerFile)
// https://pnpm.io/filtering
const [dependencyFiles, packageFiles, metaFiles] = await Promise.all([
getFilesFromPnpmSelector(`{${projectPath}}^...`, cli.root, {
extraPatterns: cli.extraPatterns
}),
getFilesFromPnpmSelector(`{${projectPath}}`, cli.root, {
// 本包目录下除了 Dockerfile 都复制
extraPatterns: cli.extraPatterns.concat([`!${cli.dockerFile}`])
}),
getMetafilesFromPnpmSelector(`{${projectPath}}...`, cli.root, {
extraPatterns: cli.extraPatterns
})
])
await withTmpDir(async (tmpdir) => {
await Promise.all([
fs.copyFile(cli.dockerFile, join(tmpdir, 'Dockerfile')),
copyFiles(dependencyFiles, join(tmpdir, 'deps')),
copyFiles(metaFiles, join(tmpdir, 'meta')),
copyFiles(packageFiles, join(tmpdir, 'pkg'))
])
const files = await getFiles(tmpdir)
if (cli.listFiles) {
for (const path of files) console.log(path)
} else {
await pipe(createTar({ gzip: true, cwd: tmpdir }, files), process.stdout)
}
})
}
// ...
# 复制构建后的代码
COPY ./meta .
COPY ./deps .
COPY ./pkg .
RUN --mount=type=cache,id=pnpm-store,target=/root/pnpm-store\
pnpm config set store-dir /root/pnpm-store && pnpm install --filter "{${PACKAGE_PATH}}..."\
--frozen-lockfile\
--prod\
--ignore-scripts\
--unsafe-perm
3.5 node-linker & package-import-method
package-import-method=copy
3.6 软链接与硬链接
04
4.1 幽灵依赖问题解决
由于依赖被打平,B 依赖被提升到 node_modules 的一级目录
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
项目只引用了 A 和 C,但是如果代码里面写了引用 B 的代码,也是可以引用到的。
{
"dependencies": {
"A": "^1.0.0",
"C": "^1.0.0"
}
}
4.2 .pnpmfile.mjs 彻底解决依赖(直接或间接)锁版本问题
// 类型信息是我根据 pnpm 源码找到添加上去的, 可以帮助编写逻辑
/**
* @typedef PackageInfo
* @type {object}
* @property {string} name
* @property {string} version
* @property {string} description
* @property {string} main
* @property {object} scripts
* @property {string} author
* @property {string} license
* @property {object} dependencies
* @property {object} devDependencies
* @property {object} optionalDependencies
* @property {object} peerDependencies
*/
/**
* @typedef LockfileProjectSnapshot Map 类型都不是真正的 Map,只是对象
* @type {object}
* @property {Map<string, string>} specifiers
* @property {Map<string, string>} dependencies
* @property {Map<string, string>} devDependencies
* @property {Map<string, string>} optionalDependencies
* @property {object} dependenciesMeta
* @property {string} publishDirectory
*/
/**
* @typedef Lockfile Map 类型都不是真正的 Map,只是对象
* @type {object}
* @property {Map<string, LockfileProjectSnapshot>} importers 引包入口
* @property {string | number} lockfileVersion
* @property {LockfileProjectSnapshot} packages
*/
/**
* @typedef ReadPackageContext
* @type {object}
* @property {function} log
*/
/**
* Will be called after parse package dependencies
* @param {PackageInfo} pkg
* @param {ReadPackageContext} context
* @returns
*/
function readPackage(pkg, context) {
// 解决直接依赖版本
if (pkg && pkg.dependencies && pkg.dependencies['@grpc/grpc-js']) {
const grpcVersion = pkg.dependencies['@grpc/grpc-js']
if (compareVersion(grpcVersion, '1.7.3') >= 1) {
pkg.dependencies['@grpc/grpc-js'] = '1.7.3'
context.log(`Modifying the @grpc/grpc-js package(which greater than 1.7.3) to be version 1.7.3 in ${pkg.name}`)
}
}
return pkg
}
/**
* Will be called before preinstall to modify lockfile to lock package version
* @param {Lockfile} lockfile
* @param {ReadPackageContext} context
* @returns
*/
function afterAllResolved(lockfile, context) {
// 直接依赖从上面 readPackage 里面来解决
// 这里是解决间接依赖的问题
/**
* 以下逻辑:
* 1. 检查所有包生产依赖有没有依赖到 @grpc/grpc-js,如有则判断版本是否超过 1.7.3,
* 如超过则强行修改依赖为 1.7.3(因为不希望引到超过这个版本,超过这个版本会有 TCP 占用问题)
* 2. 检查包本身的信息是否是 @grpc/grpc-js 这个包,如果超过则删除
* 3. 添加 @grpc/grpc-js/1.7.3 版本的 entry 信息
*/
if (lockfile.packages) {
const allPackageNames = Object.keys(lockfile.packages)
for (const packageName of allPackageNames) {
const packageInfo = lockfile.packages[packageName]
const packageDependencies = packageInfo.dependencies
if (packageName.includes('@grpc/grpc-js')) {
const grpcVersionMatchRes = packageName.match(/\\d+\\.\\d+\\.\\d+/)
const grpcVersion = grpcVersionMatchRes[0]
if (compareVersion(grpcVersion, '1.7.3') >= 1) {
delete lockfile.packages[packageName]
}
}
if (packageDependencies && packageDependencies['@grpc/grpc-js'] !== undefined) {
packageGRPCDependencyVersion = packageDependencies['@grpc/grpc-js']
// 如果版本比 1.7.3 低则不修改, 如果高于 1.7.3 版本则修改为 1.7.3
if (compareVersion(packageGRPCDependencyVersion, '1.7.3') >= 1) {
packageDependencies['@grpc/grpc-js'] = '1.7.3'
}
}
}
// 给 packages 添加 @grpc/grpc-js@1.7.3 版本 entry
lockfile.packages['/@grpc/grpc-js/1.7.3'] = grpcLockInfoEntry('1.7.3')
}
context.log('Modifying the @grpc/grpc-js package(which greater than 1.7.3) to be version 1.7.3')
return lockfile
}
...
module.exports = {
hooks: {
readPackage,
afterAllResolved
}
}
05